using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;


namespace LightCAD.Three
{
    public class TubeGeometry : BufferGeometry
    {
        public class Parameters
        {
            public QuadraticBezierCurve3 path;
            public int tubularSegments;
            public double radius;
            public int radialSegments;
            public bool closed;
        }
        #region Properties

        public Parameters parameters;
        public object tangents;
        public object normals;
        public object binormals;

        #endregion

        #region constructor
        public TubeGeometry(QuadraticBezierCurve3 path = null, int tubularSegments = 64, double radius = 1, int radialSegments = 8, bool closed = false)
        {
            if (path == null) path = new QuadraticBezierCurve3(new Vector3(-1, -1, 0), new Vector3(-1, 1, 0), new Vector3(1, 1, 0));
            this.type = "TubeGeometry";
            this.parameters = new Parameters()
            {
                path = path,
                tubularSegments = tubularSegments,
                radius = radius,
                radialSegments = radialSegments,
                closed = closed
            };
            var frames = path.computeFrenetFrames(tubularSegments, closed);
            // expose internals
            this.tangents = frames.tangents;
            this.normals = frames.normals;
            this.binormals = frames.binormals;
            // helper variables
            var vertex = new Vector3();
            var normal = new Vector3();
            var uv = new Vector2();
            var P = new Vector3();
            // buffer
            var vertices = new ListEx<double>();
            var normals = new ListEx<double>();
            var uvs = new ListEx<double>();
            var indices = new ListEx<int>();
            // create buffer data
            generateBufferData();
            // build geometry
            this.setIndex(indices.ToArray());
            this.setAttribute("position", new Float32BufferAttribute(vertices.ToArray(), 3));
            this.setAttribute("normal", new Float32BufferAttribute(normals.ToArray(), 3));
            this.setAttribute("uv", new Float32BufferAttribute(uvs.ToArray(), 2));
            // functions
            void generateBufferData()
            {
                for (int i = 0; i < tubularSegments; i++)
                {
                    generateSegment(i);
                }
                // if the geometry is not closed, generate the last row of vertices and normals
                // at the regular position on the given path
                //
                // if the geometry is closed, duplicate the first row of vertices and normals (uvs will differ)
                generateSegment((closed == false) ? tubularSegments : 0);
                // uvs are generated in a separate function.
                // this makes it easy compute correct values for closed geometries
                generateUVs();
                // finally create faces
                generateIndices();
            }
            void generateSegment(int i)
            {
                // we use getPointAt to sample evenly distributed points from the given path
                P = path.getPointAt(i / (double)tubularSegments, P);//as Vector3;
                                                                    // retrieve corresponding normal and binormal
                var N = frames.normals[i];
                var B = frames.binormals[i];
                // generate normals and vertices for the current segment
                for (int j = 0; j <= radialSegments; j++)
                {
                    var v = j / radialSegments * MathEx.PI * 2;
                    var sin = Math.Sin(v);
                    var cos = -Math.Cos(v);
                    // normal
                    normal.X = (cos * N.X + sin * B.X);
                    normal.Y = (cos * N.Y + sin * B.Y);
                    normal.Z = (cos * N.Z + sin * B.Z);
                    normal.Normalize();
                    normals.Push(normal.X, normal.Y, normal.Z);
                    // vertex
                    vertex.X = P.X + radius * normal.X;
                    vertex.Y = P.Y + radius * normal.Y;
                    vertex.Z = P.Z + radius * normal.Z;
                    vertices.Push(vertex.X, vertex.Y, vertex.Z);
                }
            }
            void generateIndices()
            {
                for (int j = 1; j <= tubularSegments; j++)
                {
                    for (int i = 1; i <= radialSegments; i++)
                    {
                        var a = (radialSegments + 1) * (j - 1) + (i - 1);
                        var b = (radialSegments + 1) * j + (i - 1);
                        var c = (radialSegments + 1) * j + i;
                        var d = (radialSegments + 1) * (j - 1) + i;
                        // faces
                        indices.Push(a, b, d);
                        indices.Push(b, c, d);
                    }
                }
            }
            void generateUVs()
            {
                for (int i = 0; i <= tubularSegments; i++)
                {
                    for (int j = 0; j <= radialSegments; j++)
                    {
                        uv.X = i / tubularSegments;
                        uv.Y = j / radialSegments;
                        uvs.Push(uv.X, uv.Y);
                    }
                }
            }
        }
        #endregion

        #region methods
        public TubeGeometry copy(TubeGeometry source)
        {
            base.copy(source);
            this.parameters = new Parameters()
            {
                path = source.parameters.path,
                tubularSegments = source.parameters.tubularSegments,
                radius = source.parameters.radius,
                radialSegments = source.parameters.radialSegments,
                closed = source.parameters.closed,
            };
            return this;
        }
        #endregion
    }
}
